﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.IO;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using Phoenix.ReflectionExtensions;
using System.IO.Compression;
using MessageShark;
using System.Collections.Concurrent;
using System.Reflection.Emit;
using System.Reflection;
using PServiceBus.Serializer.Xml;


namespace PServiceBus.Core.Runtime.Extensions {
    /// <summary>
    /// Extension method for object
    /// </summary>
    public static class ObjectExtension {
        /// <summary>
        /// Cast object to another Type.
        /// </summary>
        /// <typeparam name="T">The type</typeparam>
        /// <param name="value">The value</param>
        /// <returns>Specified type</returns>
        public static T CastTo<T>(this object value) {
            return value.CastTo<T>(default(T));
        }
        /// <summary>
        /// Cast object to another type and return default if object is null.
        /// </summary>
        /// <typeparam name="T">The type</typeparam>
        /// <param name="value">The value</param>
        /// <param name="defValue">default to return when value is null</param>
        /// <returns>Specified type</returns>
        public static T CastTo<T>(this object value, T defValue) {
            T result = defValue;
            try {
                result = (T) value.CastTo(typeof(T));
            } catch { }
            return result;
        }

        /// <summary>
        /// Cast object to another type using the specified type
        /// </summary>
        /// <param name="value"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static object CastTo(this object value, Type type) {
            object result = default(object);
            if (value == null || value == DBNull.Value) return result;
            try {
                result = type.IsEnum ? Enum.Parse(type, value.ToString()) : Convert.ChangeType(value, type);
            }
            catch { }
            return result;
        }

        /// <summary>
        /// Make a deep copy of object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static T DeepClone<T>(this T value) {
            //Reverse to old logic to prevent concurrency issue not solved yet in message shark
            using (var memStream = new MemoryStream()) {
                var binaryFormatter = new BinaryFormatter(null,
                     new StreamingContext(StreamingContextStates.Clone));
                binaryFormatter.Serialize(memStream, value);
                memStream.Seek(0, SeekOrigin.Begin);
                return binaryFormatter.Deserialize(memStream).CastTo<T>();
            }
            //var buffer = MessageSharkSerializer.Serialize<T>(value);
            //return MessageSharkSerializer.Deserialize<T>(buffer);
        }

        /// <summary>
        /// Convert the specified type into a dictionary[key,value]
        /// </summary>
        /// <typeparam name="TObject"></typeparam>
        /// <param name="message"></param>
        /// <returns></returns>
        public static Dictionary<string, object> ToDict<TObject>(this TObject message) {
            return ToDictFunc(message.GetType())(message);
        }

        /// <summary>
        /// Generate Class name based on dict structure
        /// </summary>
        /// <param name="dict"></param>
        /// <returns></returns>
        public static string GenerateClassName(this Dictionary<string, object> dict) {
            var name = String.Concat("Class", String.Join(string.Empty, dict.Select(kv => String.Concat(kv.Key, GenerateClassNameForObject(kv.Value)))));
            return String.Concat("ca", name.GetHashCode().ToString());
        }

        private static string GenerateClassNameForObject(object value) {
            var type = value.GetType();
            if (_dictType.IsAssignableFrom(type)) return GenerateClassName(value as Dictionary<string, object>);
            return type.Name;
        }


        private static readonly ConcurrentDictionary<Type, Func<object, Dictionary<string, object>>> _dictFuncs =
            new ConcurrentDictionary<Type, Func<object, Dictionary<string, object>>>();


        private static readonly string _toDictStr = String.Intern("ToDict");
        private static readonly Type _dictType = typeof(Dictionary<string, object>);
        private static readonly ConstructorInfo _dictCtor = _dictType.GetConstructor(Type.EmptyTypes);
        private static readonly MethodInfo _setItemDictType = _dictType.GetMethod("set_Item");
        private static readonly Type[] _toDictParamTypes = new[] { typeof(object) };
        private static readonly Type _toDictFuncType = typeof(Func<object, Dictionary<string, object>>);
        private static readonly MethodInfo _translateMethod = typeof(ObjectExtension).GetMethod("Translate",
             BindingFlags.Static | BindingFlags.Public);

        private static object TranslateEx(object value) {
            return value.Translate();
        }

        private static Func<object, Dictionary<string, object>> GetToDictFunc(Type type) {
            var methodName = String.Concat(_toDictStr, type.FullName.Replace(".", string.Empty));
            var dynamicMethod = new DynamicMethod(methodName, _dictType, _toDictParamTypes, true);
            var il = dynamicMethod.GetILGenerator();

            var dictLocal = il.DeclareLocal(_dictType);
            il.Emit(OpCodes.Newobj, _dictCtor);
            il.Emit(OpCodes.Stloc, dictLocal.LocalIndex);

            var props = type.GetPropertiesEx();

            foreach (var prop in props) {
                if (!prop.CanRead) continue;
                il.Emit(OpCodes.Ldloc, dictLocal.LocalIndex);
                il.Emit(OpCodes.Ldstr, prop.Name);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Castclass, type);
                il.Emit(OpCodes.Callvirt, prop.GetGetMethod());
                if (prop.PropertyType.IsValueType)
                    il.Emit(OpCodes.Box, prop.PropertyType);
                il.Emit(OpCodes.Call, _translateMethod);
                il.Emit(OpCodes.Callvirt, _setItemDictType);
            }

            il.Emit(OpCodes.Ldloc, dictLocal.LocalIndex);
            il.Emit(OpCodes.Ret);
            return dynamicMethod.CreateDelegate(_toDictFuncType)
                as Func<object, Dictionary<string, object>>;
        }

        private static Func<object, Dictionary<string, object>> ToDictFunc(Type type) {
            return _dictFuncs.GetOrAdd(type, k => GetToDictFunc(k));
        }
       

        /// <summary>
        /// Gets the value from hash table based on key and convert value to specified type.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        /// <param name="key">The key.</param>
        /// <returns>Specified type</returns>
        public static T Get<T>(this Hashtable value, string key) {
            return value.Contains(key) ? value[key].CastTo<T>() : default(T);
        }

        /// <summary>
        /// Gets the value from dictionary based on key and convert value to specified type.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        /// <param name="key">The key.</param>
        /// <returns>Specified type</returns>
        public static T Get<T>(this IDictionary value, string key) {
            return value.Contains(key) ? value[key].CastTo<T>() : default(T);
        }

        /// <summary>
        /// Get value from dictionary if exists else returns default value
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="value"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static TValue Select<TKey, TValue>(this IDictionary<TKey, TValue> value, TKey key) {
            return value.ContainsKey(key) ? value[key] : default(TValue);
        }


        /// <summary>
        /// Convert the specified dictionary into a dynamic object.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>dynamic object</returns>
        public static dynamic AsDynamic<TKey,TValue>(this IDictionary<TKey,TValue> value) {
            dynamic result = new DynamicPropertyBag();
            foreach (var d in value)
                result[d.Key.ToString()] = d.Value.ToExpectedType();
            return result;
        }

        /// <summary>
        /// Merges the specific dictionary with the underlying dictionary and return a new hash table.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="dict">The table.</param>
        /// <returns>hash table</returns>
        public static Dictionary<TKey, TValue> MergeWith<TKey,TValue>(this IDictionary<TKey, TValue> value, Dictionary<TKey, TValue> dict) {
            var newDict = new Dictionary<TKey, TValue>();
            value = value ?? new Dictionary<TKey, TValue>();
            dict = dict ?? new Dictionary<TKey, TValue>();
            foreach (var d in value) newDict[d.Key] = d.Value;
            foreach (var d in dict) newDict[d.Key] = d.Value;
            return newDict;
        }

        /// <summary>
        /// Return true if List is empty. Null value are not counted as values for check for empty
        /// e.x. If all the elements are null, then it will return true
        /// </summary>
        /// <param name="list"></param>
        /// <returns></returns>
        public static bool IsEmpty(this IList list) {
            var count = 0;
            foreach (var item in list) 
                if(item != null) count++;
            return count == 0;
        }

        
        /// <summary>
        /// Try to convert the specific object to the correct type.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>return correct object type</returns>
        public static object ToExpectedType(this object value) {
            if (value == null || value.GetType() != typeof(String)) return value;
            return AutomaticTypeConverter.ToExpectedType(value.ToString());
        }

        private static Type byteArrayType = typeof(byte[]),
                        stringType = typeof(string);

        /// <summary>
        /// Translate object into a format that can be passed through the pipe using internal serialization
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static object Translate(this object obj) {
            if (obj == null) return obj;
            var type = obj.GetType();
            if (stringType == type) return obj.ToString().Escape();
            if (byteArrayType == type) return Convert.ToBase64String(obj as byte[]);
            if (type.IsEnum) return Convert.ChangeType(obj, typeof(int));
            if (type.CanInitType()) {
                var xml = XmlSerializer.Serialize(obj);
                var payload = System.Text.Encoding.UTF8.GetBytes(xml.InnerXml());
                return Convert.ToBase64String(payload) + "[xmlbase64]";
            }
            return obj;
        }

        /// <summary>
        /// Return true if type is not a primitive type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static bool CanInitType(this Type type) {
            return type.GetConstructor(Type.EmptyTypes) != null || type.IsArray;
        }

        /// <summary>
        /// Return true if object is valid for xml serialization
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static bool IsValidForXml(this object obj) {
            return obj.GetType().CanInitType();
        }

        /// <summary>
        /// Convert object to xml using custom xml serializer
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static string ToXml(this object obj) {
            return XmlSerializer.Serialize(obj);
        }


        /// <summary>
        /// Extension to provide the same functionality as IList.ForEach
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="list"></param>
        /// <param name="action"></param>
        public static void Each<T>(this IEnumerable<T> list, Action<T> action) {
            foreach (var item in list) action(item);
        }

        private static readonly Type StringType = typeof(string);
        private static readonly Encoding UTF8 = Encoding.UTF8;

        public static byte[] Serialize<T>(this T obj) {
            if (obj == null) return null;
            if (typeof(T) == StringType) {
                var value = obj as string;
                var size = UTF8.GetByteCount(value);
                var buffer = new byte[size];
                UTF8.GetBytes(value, 0, value.Length, buffer, 0);
                return buffer;
            }
            return MessageSharkSerializer.Serialize(obj);
        }

        public static T Deserialize<T>(this byte[] buffer) {
            if (typeof(T) == StringType) return (T)(UTF8.GetString(buffer) as object);
            return MessageSharkSerializer.Deserialize<T>(buffer);
        }
    }
}
